jasny/switch-route

v0.2.2 2024-08-29 21:56 UTC

This package is auto-updated.

Last update: 2024-09-03 20:44:30 UTC


README

jasny-banner

SwitchRoute

Build Status Scrutinizer Code Quality Code Coverage Packagist Stable Version Packagist License

生成PHP脚本以实现更快的路由。

传统的路由方式使用正则表达式。这种方法通过FastRoute得到改进,它将所有路由编译成一个正则表达式。 SwitchRoute 完全放弃了这种方法,转而使用一系列 switch 语句。

处理路由以生成 switch 语句并不特别快。然而,生成只需发生一次,而不是每次请求。

============================= Average Case (path) =============================

SwitchRoute            100% | ████████████████████████████████████████████████████████████  |
FastRoute (cache)       59% | ███████████████████████████████████                           |
SwitchRoute (psr)       20% | ███████████                                                   |
Symfony                  2% | █                                                             |
FastRoute                1% |                                                               |
Laravel                  1% |                                                               |

查看所有基准测试结果

安装

composer require jasny/switch-route

需要PHP 8.1+

使用

在所有示例中,我们将使用以下函数来获取路由;

function getRoutes(): array
{
    return [
        'GET      /'                  => ['controller' => 'InfoController'],

        'GET      /users'             => ['controller' => 'UserController', 'action' => 'listAction'],
        'POST     /users'             => ['controller' => 'UserController', 'action' => 'addAction'],
        'GET      /users/{id}'        => ['controller' => 'UserController', 'action' => 'getAction'],
        'POST|PUT /users/{id}'        => ['controller' => 'UserController', 'action' => 'updateAction'],
        'DELETE   /users/{id}'        => ['controller' => 'UserController', 'action' => 'deleteAction'],

        'GET      /users/{id}/photos' => ['action' => 'ListPhotosAction'],
        'POST     /users/{id}/photos' => ['action' => 'AddPhotosAction'],

        'POST     /export'            => ['include' => 'scripts/export.php'],
    ];
}

支持 {id}:id 语法来捕获URL路径变量。如果段可以是任何内容,但不需要捕获,请使用 *(例如 /comments/{id}/*)。

不支持在路径变量上使用正则表达式(例如 {id:\d+})。

在调用操作时,可以使用路径变量作为参数。反射用于确定参数的名称,这些名称与路径变量的名称进行匹配。

class UserController
{
    public function updateAction(string $id)
    {
        // ...
    }
}

注意,路径变量始终是字符串。

漂亮的控制器和操作名称

默认情况下,应使用完全限定的类名(包括命名空间)配置 controlleraction。但是,可以使用一个漂亮的名字代替,并让 Invoker 将其转换为 fqcn。

function getRoutes(): array
{
    return [
        'GET      /'                  => ['controller' => 'info'],

        'GET      /users'             => ['controller' => 'user', 'action' => 'list'],
        'POST     /users'             => ['controller' => 'user', 'action' => 'add'],
        'GET      /users/{id}'        => ['controller' => 'user', 'action' => 'get'],
        'POST|PUT /users/{id}'        => ['controller' => 'user', 'action' => 'update'],
        'DELETE   /users/{id}'        => ['controller' => 'user', 'action' => 'delete'],

        'GET      /users/{id}/photos' => ['action' => 'list-photos'],
        'POST     /users/{id}/photos' => ['action' => 'add-photos'],

        'POST     /export'            => ['include' => 'scripts/export.php'],
    ];
}

Invoker 传递一个可调用的函数,该函数将转换漂亮的控制器和操作名称

$stud = fn($str) => strtr(ucwords($str, '-'), ['-' => '']);
$camel = fn($str) => strtr(lcfirst(ucwords($str, '-')), ['-' => '']);

$invoker = new Invoker(function (?string $controller, ?string $action) use ($stud, $camel) {
    return $controller !== null
        ? [$stud($controller) . 'Controller', $camel($action ?? 'default') . 'Action']
        : [$stud($action) . 'Action', '__invoke'];
});

基本

默认情况下,生成器会生成一个路由请求的函数。

use Jasny\SwitchRoute\Generator;

// Always generate in development env, but not in production.
$overwrite = (getenv('APPLICATION_ENV') ?: 'dev') === 'dev';

$generator = new Generator();
$generator->generate('route', 'generated/route.php', 'getRoutes', $overwrite);

要路由,包含生成的文件并调用 route 函数。

require 'generated/route.php';

$method = $_SERVER["REQUEST_METHOD"];
$path = rawurldecode(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH));

route($method, $path);

PSR-15 兼容的中间件

PSR-7 是HTTP请求的抽象。它允许您更容易地测试应用程序。它通过 http_message 扩展 或许多PSR-7库提供。

相关的 PSR-15 描述了通过处理程序和中间件处理 ServerRequest 对象的方式。这使得抽象应用程序变得更容易。

该库可以生成实现 PSR-15 MiddlewareInterface 的类。

生成的路由中间件将根据匹配的路由设置 ServerRequest 的属性。生成的调用中间件将实例化控制器并调用操作。

这被分成两个步骤,这样您就可以添加在路由确定后但在调用之前执行的中间件。

use Jasny\SwitchRoute\Generator;
use Jasny\SwitchRoute\Invoker;

// Always generate in development env, but not in production.
$overwrite = (getenv('APPLIACTION_ENV') ?: 'dev') === 'dev';

$routeGenerator = new Generator(new Generator\GenerateRouteMiddleware());
$routeGenerator->generate('App\Generated\RouteMiddleware', 'generated/RouteMiddleware.php', 'getRoutes', $overwrite);

$invoker = new Invoker();
$invokeGenerator = new Generator(new Generator\GenerateInvokeMiddleware($invoker));
$invokeGenerator->generate('App\Generated\InvokeMiddleware', 'generated/InvokeMiddleware.php', 'getRoutes', $overwrite);

使用任何PSR-15兼容的请求分发器,如Relay来处理请求。

use App\Generated\RouteMiddleware;
use App\Generated\InvokeMiddleware;
use Jasny\SwitchRoute\NotFoundMiddleware;
use HttpMessage\Factory as HttpFactory;
use HttpMessage\ServerRequest;
use Relay\Relay;

$httpFactory = new HttpFactory();

$middleware[] = new RouteMiddleware();
$middleware[] = new NotFoundMiddleware($httpFactory);
$middleware[] = new InvokeMiddleware(fn($controllerClass) => new $controllerClass($httpFactory));

$relay = new Relay($middleware);

$request = new ServerRequest($_SERVER, $_COOKIE, $_QUERY, $_POST, $_FILES);
$response = $relay->handle($request);

依赖注入

通常,您希望使用DI(依赖注入)容器,可选地使用自动装配,而不是简单地进行 new 来创建控制器。

use App\Generated\InvokeMiddleware;

$container = new DI\Container();

$middleware[] = new InvokeMiddleware(fn($controllerClass) => $container->get($controllerClass));

错误页面

如果没有路由与当前请求URI匹配,生成的脚本或调用者将给出 404 Not Found405 Method Not Allowed 响应。当有匹配的端点但没有任何方法匹配时,会给出 405 响应。响应将包含简单的文本正文。

要更改此行为,请创建一个 default 路由。

function getRoutes(): array
{
    return [
        'GET /'   => ['controller' => 'info'],
        // ...

        'default' => ['controller' => 'error', 'action' => 'not-found],
    ];
}

如果方法应该有 array $allowedMethods 作为函数参数。如果数组为空,则应返回 404 Not Found 响应。否则,可能会返回 405 Method Not Allowed 响应,包括一个带有允许方法的 Allow 标头。

class ErrorController
{
    public function notFoundAction(array $allowedMethods)
    {
        if ($allowedMethods === []) {
            http_response_code(404);
        } else {
            http_response_code(405);
            header('Allow: ' . join(', ', $allowedMethods));
        }
        
        echo "<h1>Sorry, there is nothing here</h1>";
    }
}

此示例展示了不使用 PSR-7 ServerRequest 的简单实现。

预生成的路由脚本

如果生成的文件已经存在,SwitchRoute 的开销已经最小。为了在生产环境中达到零开销,每次部署新版本时都应提前生成类或脚本。

创建一个脚本 bin/generate-router.php

require_once 'config/routes.php';

(new Jasny\SwitchRoute\Generator)->generate('route', 'generated/route.php', 'getRoutes', true);

将其添加到 composer.json 中,以便在每次更新自动加载(通过运行 composer updatecomposer install)之后调用。

{
    "scripts": {
        "post-autoload-dump": [
            "bin/generate-router.php"
        ]
    }
}

文档

生成器

Generator 是一个基于一组路由生成 PHP 脚本的服务。

每个路由是一个键值对,其中键是 HTTP 方法和 URL 路径。值是一个数组,应包含一个 controller 和(可选的)一个 action 属性,或者一个 include 属性。

对于具有 include 属性的路由,脚本简单地包含进来。使用 include 提供了一种将路由添加到旧版应用程序的方法。

对于具有 controller 属性的路由,将实例化控制器,并调用操作,使用从 URL 解析的参数作为参数。

生成器将一个可调用的对象作为构造参数,该参数用于从结构化路由生成 PHP 代码。这是来自该库的 invokable Generator\Generate... 对象或一个 自定义生成函数

new Generator(new Generator\GenerateRouteMiddleware());

如果没有提供生成可调用对象,将创建一个新的 GenerateFunction 对象(可选 DI)。

该类有一个单一的方法 generate() 和没有公共属性。

Generator::generate()

Generator::generate() 将创建一个 PHP 脚本来路由请求。该脚本写入指定的文件,应通过 require(或自动加载)包含。

Generator::generate(string $name, string $filename, callable $getRoutes, bool $force)

参数 $name 是生成的函数或类的名称,可能包括命名空间。

而不是传递路由作为参数,使用回调来获取路由。如果路由器没有被替换,则不会调用此回调。

可选的 force 参数默认为 true,这意味着每次调用此方法时都会生成新的脚本。在生产环境中,您应将其设置为 false 并在每次更新应用程序时删除生成的文件。

如果 forcefalse 且文件已存在,则不会生成新文件。建议安装并启用 opcache zend 扩展。这防止了在每个请求上对文件系统的额外检查。

Generator\GenerateFunction

GenerateFunction 是一个可调用的类,它作为函数生成,将调用路由指定的操作或包含脚本。

此类不使用 PSR-7 或任何其他请求抽象。相反,它将直接实例化控制器并调用操作,传递正确的路径段作为参数。

在实例化此可调用对象时,您应传递一个 Invoker 对象。如果不传递,则会在构造过程中自动创建一个(可选 DI)。

在调用 generate 时,您传递路由函数的名称。这可能包括命名空间。

use Jasny\SwitchRoute\Generator;
use Jasny\SwitchRoute\Invoker;

$invoker = new Invoker();
$generate = new Generator\GenerateFunction($invoker);

$generator = new Generator($generate);
$generator->generate('route', 'generated/route.php', 'getRoutes', true);

生成的函数接受两个参数,第一个是请求方法,第二个是请求路径。路径不会直接在 $_SERVER 中可用,但需要从 $_SERVER['REQUEST_URI'] 中提取,其中也包含查询字符串。

require 'generated/route.php';

$method = $_SERVER["REQUEST_METHOD"];
$path = rawurldecode(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH));

route($method, $path);

函数返回操作方法返回的内容。

Generator\GenerateRouteMiddleware

Generator\GenerateRouteMiddleware 是一个可调用的函数,用于生成一个中间件类,该类将确定路由并设置路由属性和路径变量作为 PSR-7 ServerRequest 属性。该中间件实现了 PSR-15 MiddlewareInterface

在调用 generate 时,您需要传入中间件类的名称。这可以包括命名空间。

use Jasny\SwitchRoute\Generator;

$generate = new Generator\GenerateRouteMiddleware();

$generator = new Generator($generate);
$generator->generate('RouteMiddleware', 'generated/RouteMiddleware.php', 'getRoutes', true);

路由仅通过 $request->getMethod()$request->getUri()->getPath() 来确定。

中间件将路由的属性,如 controlleraction,通过 $request->addAttribute() 添加到 ServerRequest 中。名称前缀为 route: 以防止与其他中间件发生冲突。因此,action 变为属性 route:action

除了调用动作所需的属性外,还可以指定其他属性,这些属性可以由自定义中间件或控制器处理。例如,包含所需授权级别的 auth。请注意,这些属性始终以 route: 前缀开头,因此 auth 变为 route:auth

路径变量格式为 route:{...}(例如 route:{id})。这意味着它们不会与路由参数冲突。为了指定动作参数的固定值,需要在这些花括号中指定路由参数。

[
    'GET /users/{id}/photos'        => ['action' => 'ListPhotosAction', '{page}' => 1],
    'GET /users/{id}/photos/{page}' => ['action' => 'ListPhotosAction'],
];

如果与 URL 路径匹配,中间件将设置 route:methods_allowed 属性,而不论 HTTP 请求方法如何。这对于返回 405 Method Not Allowed 非常有用。

Generator\GenerateInvokeMiddleware

Generator\GenerateInvokeMiddleware 是一个可调用的函数,用于生成一个中间件类,该类基于 ServerRequest 属性调用动作。该中间件实现了 PSR-15 MiddlewareInterface

在实例化此可调用对象时,您应传递一个 Invoker 对象。如果不传递,则会在构造过程中自动创建一个(可选 DI)。

在调用 generate 时,您需要传入中间件类的名称。这可以包括命名空间。

use Jasny\SwitchRoute\Generator;

$generate = new Generator\GenerateInvokeMiddleware();

$generator = new Generator($generate);
$generator->generate('InvokeMiddleware', 'generated/InvokeMiddleware.php', 'getRoutes', true);

生成的类接受一个单例可调用对象作为可选的构造函数参数。此可调用对象用于实例化控制器或动作,并允许您进行依赖注入。如果省略,则使用简单的 new 语句。

应在生成的路由中间件之后使用调用中间件。可以在这些两个中间件之间添加其他(自定义)中间件,这些中间件可以基于路由中间件设置的 server 请求属性执行操作。

请注意,所有控制器和动作调用都是预先生成的。请求属性如 route:controllerroute:action 不应修改。

对于指定 include 属性的路由,脚本将被简单地包含,不会执行其他方法调用。

应指定默认路由或使用 NotFoundMiddleware。如果两者都没有执行,则在没有匹配的路由时生成的调用中间件将抛出 LogicException

NotFoundMiddleware

NotFoundMiddleware 如果没有匹配的路由且未指定默认路由,将返回 404 Not Found405 Method Not Allowed 响应。

中间件接受一个实现 PSR-17 ResponseFactoryInterface 的对象作为构造函数参数。此工厂用于在未找到匹配的路由时创建响应。

属性 route:allowed_methods 决定响应代码。如果没有为 URL 指定允许的方法,则返回 404 响应。如果有,则返回 405

生成的响应将包含一个简单的文本主体,带有 HTTP 状态原因短语。在 405 响应的情况下,中间件还将设置 Allow 标头。

Invoker

Invoker 根据所选路由生成调用动作或包含文件的片段。这包括将 controller 和/或 action 属性转换为类名和可能的方法名。

默认情况下,如果路由有 controller 属性,则将其用作类名。将 action 属性用作方法。默认为 defaultAction

如果只存在 action 属性,调用者将使用它作为类名。该类必须定义一个 可调用对象

您可以通过向构造函数传递一个回调来更改可调用类和方法名称的生成方式。这可以用于将控制器和操作类的漂亮名称转换为完全限定类名(FQCN)。

$stud = fn($str) => strtr(ucwords($str, '-'), ['-' => '']);
$camel = fn($str) => strtr(lcfirst(ucwords($str, '-')), ['-' => '']);

$invoker = new Invoker(function (?string $controller, ?string $action) use ($stud, $camel) {
    return $controller !== null
        ? ['App\\' . $stud($controller) . 'Controller', $camel($action ?? 'default') . 'Action']
        : ['App\\' . $stud($action) . 'Action', '__invoke'];
});

调用者使用反射来确定方法是否为静态的。如果方法不是静态的,则...

反射还用于找出可调用对象的参数名称。这些名称与路径变量(如 {id} => $id)的名称相匹配。

NoInvoker

可以使用 NoInvoker 替代 Invoker 来返回匹配的路由,而不是调用动作。这主要用于基准测试和测试。

这应该与 GenerateFunction 结合使用。当使用 PSR-7 时,您可以通过仅使用路由中间件而不调用中间件来实现类似的功能。

use Jasny\SwitchRoute\Generator;
use Jasny\SwitchRoute\NoInvoker;

$invoker = new NoInvoker();
$generate = new Generator\GenerateFunction($invoker);

$generator = new Generator($generate);
$generator->generate('route', 'generated/route.php', 'getRoutes', true);

生成的函数的结果是一个包含 3 个元素的数组。第一个包含 HTTP 状态,第二个包含路由属性,第三个包含路径变量。

require 'generated/route.php';

$method = $_SERVER["REQUEST_METHOD"];
$path = rawurldecode(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH));

$routeInfo = route($method, $path);

switch ($routeInfo[0]) {
    case 404:
        // ... 404 Not Found
        break;
    case 405:
        $allowedMethods = $routeInfo[1];
        // ... 405 Method Not Allowed
        break;
    case 200:
        $route = $routeInfo[1];
        $vars = $routeInfo[2];
        // ... invoke action based on $route using $vars
        break;
}

自定义

创建 Generator 类时,可以传递任何可调用。这个可调用应该具有以下签名;

use Jasny\SwitchRoute\Generator;

$generate = function (string $name, array $routes, array $structure): string {
    // ... custom logic
    return $generatedCode;
};

$generator = new Generator($generate);
$generator->generate('route', 'generated/route.php', 'getRoutes', true);

通过调用 $getRoutes 可调用,由 Generator 收集 $routes。基于这些路由计算出的结构,通过将路由分割成部分。每个叶节点有一个键 "\0" 和一个 Endpoint 对象作为值。

自定义调用者

标准 Invoker 可以被实现 InvokableInterface 的类替换。该接口描述了 2 个方法;generateInvocation()generateDefault()

generateInvocation() 为给定路由生成实例化和调用动作的代码。它接受 3 个参数;

  • 匹配的路由(作为数组)
  • 用于生成代码的回调,该代码通过路径变量将方法参数转换为路径段
  • 实例化类的 PHP 代码,其中 %s 应替换为类名
$invoker->generateInvocation($route, function ($name, $type = null, $default = null) { /* ... */ }, '(new %s)'); 

generateDefault() 不接受任何参数,应返回在没有匹配路由且未生成默认路由的情况下应返回的代码。在生成中间件时不会调用此方法。