snicco/http-routing

snicco 框架的 HTTP 和路由组件

v2.0.0-beta.9 2024-09-07 14:27 UTC

README

codecov Psalm Type-Coverage Psalm level PhpMetrics - Static Analysis PHP-Versions

HTTP-Routing 组件是 Snicco 项目 的一个有观点的库,它结合了基于 FastRoute 构建的路由系统和强大的 PSR-15 中间件调度器。

虽然不是必需的,但它被特意构建以支持像 WordPress 这样的旧版 CMS,在这些 CMS 中,您无法完全控制请求-响应生命周期。

特性

  • 丰富的路由配置 API
  • URL 生成/反向路由
  • 按路由附加中间件
  • 路由分组
  • 在生产环境中完全缓存
  • 针对旧版 CMS 的管理区域进行特殊处理(如果适用)
  • 等等。

目录

  1. 安装
  2. 路由
    1. 创建路由器
    2. 定义路由
      1. 定义 HTTP 动词
      2. 路由参数
      3. 正则表达式约束
      4. 添加中间件
      5. 添加条件
      6. 路由分组
      7. 控制器
      8. 重定向路由
      9. 视图路由
      10. 管理路由
      11. API 路由
      12. 路由缓存
    3. 匹配路由
    4. 反向路由/URL 生成
    5. 管理菜单
  3. PSR-15 中间件调度器
    1. 创建中间件管道
    2. 管道请求
    3. 中间件解析器
    4. PSR 工具
  4. 贡献
  5. 问题和 PR
  6. 安全性

安装

composer require snicco/http-routing

路由

创建路由器

路由子组件的中心类是 Router 门面类。(不是 Laravel 门面)

Router 作为一个工厂,为路由系统的不同部分提供服务。

为了实例化一个 Router,我们需要以下合作者

  • URLGenerationContext,这是一个配置 URL 生成的 值对象
  • RouteLoader,负责加载和配置您的路由(如果还没有缓存的话。)
  • RouteCache,负责在生成环境中缓存路由定义。
  • AdminArea 的实例,它作为路由系统和旧版 CMS 管理区域之间的桥梁。
use Snicco\Component\HttpRouting\Routing\Cache\FileRouteCache;use Snicco\Component\HttpRouting\Routing\Cache\NullCache;
use Snicco\Component\HttpRouting\Routing\RouteLoader\DefaultRouteLoadingOptions;
use Snicco\Component\HttpRouting\Routing\RouteLoader\PHPFileRouteLoader;
use Snicco\Component\HttpRouting\Routing\Router;
use Snicco\Component\HttpRouting\Routing\UrlGenerator\UrlGenerationContext;

$context = new UrlGenerationContext('snicco.io');

$route_loading_options = new DefaultRouteLoadingOptions(
    '/api/v1' // the base-prefix for API routes
);
$route_loader = new PHPFileRouteLoader(
    [__DIR__.'/routes'], // directories of "normal" routes
    [__DIR__.'/routes/api'], // directories of "API" routes, optional
    $route_loading_options,
);

// during development
$route_cache = new NullCache();
// during production
$route_cache = new FileRouteCache('/path/to/cache_dir/route_cache.php');

$router = new Router(
     $context,
     $route_loader,
     $route_cache
//     $admin_area  This is a simple interface that you can implement if you use admin routes.
);

一旦我们有了我们的 Router,我们就可以使用它来实例化路由系统的不同部分。

use Snicco\Component\HttpRouting\Routing\Router;

/**
* @var Router $router 
*/
$router = /* */

$router->routes(); // Returns an instance of RouteCollection

$router->urlGenerator(); // Returns an instance of UrlGenerator

$router->urlMatcher(); // Returns an instance of UrlMatcher

$router->adminMenu(); // Returns an instance of AdminMenu

定义路由

包含的 PHPFileRouteLoader 将在提供的路由目录中的每个目录内搜索具有 .php 扩展名的文件。不使用嵌套目录。

目前,我们假设以下目录结构

your-project-root
├── routes/
│   ├── frontend.php
│   ├── admin.php
├── api-routes/
│   ├── v1.php
└── ...

每个路由目录内的文件都必须返回一个闭包,该闭包接受一个 RoutingConfigurator 实例。

// ./routes/frontend.php
use Snicco\Component\HttpRouting\Routing\RoutingConfigurator\WebRoutingConfigurator;

return function (WebRoutingConfigurator $configurator ) {
    //
}

特殊情况下,admin.php 路由文件将接收一个 AdminRoutingConfigurator 实例。

// ./routes/admin.php
use Snicco\Component\HttpRouting\Routing\RoutingConfigurator\AdminRoutingConfigurator;

return function (AdminRoutingConfigurator $configurator ) {
    //
}

RouteLoadingOptions 是一个 值对象,允许您为所有路由自定义一些通用设置,例如自动添加具有路由文件名称的中介。

查看 DefaultRouteLoadingOptions 以获取示例。

定义 HTTP 动词

use Snicco\Component\HttpRouting\Routing\RoutingConfigurator\WebRoutingConfigurator;

/**
* @var WebRoutingConfigurator $configurator 
*/
$configurator = /* */

$configurator->get(
   'posts.index', // The route name MUST BE UNIQUE.
   '/posts', // The route pattern
   [PostController::class, 'index'] // The controller for the route.
);

$configurator->post('posts.create', '/posts', [PostController::class, 'create']);

$configurator->put('posts.update', '/posts/{post_id}', [PostController::class, 'update']);

$configurator->delete('posts.delete', '/posts/{post_id}', [PostController::class, 'delete']);

$configurator->patch(/* */);

$configurator->options(/* */);

$configurator->any(/* */);

$configurator->match(['GET', 'POST'], /* */);

路由参数

HTTP-Routing 组件的语法提供了一种替代原生语法的语法。这非常具有意见性,但我们认为 FastRoute 的语法有些冗长,尤其是在处理可选段和正则表达式要求时。

为了获得最佳性能,所有路由将在缓存之前编译以匹配 FastRoute 的原生语法。

  • {...} 中包含的路由段是必需的。
  • {...?} 中包含的路由段是可选的。
$configurator->get(
   'route_name',
   '/posts/{post}/comments/{comment?}',
   PostController::class
);

上述路由定义将匹配 /posts/1/comments/2/posts/1/comments

捕获的参数将可用于配置的控制器。

可以使用路由段组合使用尾部斜杠。

$configurator->get(
   'route_name',
   '/posts/{post}/comments/{comment?}/',
   PostController::class
);

上述路由定义将匹配 /posts/1/comments/2//posts/1/comments/

可选段只能出现在路由模式的末尾。

正则表达式约束

use Snicco\Component\HttpRouting\Routing\Route\Route;
use Snicco\Component\HttpRouting\Routing\RoutingConfigurator\WebRoutingConfigurator;

/**
* @var WebRoutingConfigurator $configurator 
*/
$configurator->get(
   'route1',
   '/user/{id}/{name}',
    PostController::class
)->requirements([
    'id' => '[0-9]+',
    'name' => '[a-z]+'
]);

/** @var Route $route */
$route = $configurator->get(/* */);

// The Route class contains a couple of helper methods.
$route->requireAlpha('segment_name');
$route->requireNum('segment_name');
$route->requireAlphaNum('segment_name');
$route->requireOneOf('segment_name', ['category-1', 'category-2']);

添加中介

可以为每个路由单独配置中介。

中介可以是 PSR-15 中介 的完全限定类名或稍后将解析为 PSR-15 中介类名的别名。

可以将参数作为逗号分隔的列表传递给中介(构造函数),并在 : 之后传递。在实例化中介并传递参数之前执行以下转换

  • (string) true => (bool) true
  • (string) false => (bool) false
  • (string) numeric => numeric
use Snicco\Component\HttpRouting\Routing\Route\Route;
use Snicco\Component\HttpRouting\Routing\RoutingConfigurator\WebRoutingConfigurator;

/**
* @var WebRoutingConfigurator $configurator 
*/
$configurator->get('route1', '/route1', InvokableController::class)
              // middleware as an alias.
             ->middleware('auth')
             
             // adding multiple middleware
             ->middleware([PSR15MiddlewareOne::class, PSR15MiddlewareTwo::class]);
             
             // passing comma separated arguments
             ->middleware('can:manage_options,1');

添加条件

除了通过其 URL 模式匹配路由外,还可以指定路由条件。

路由条件是任何实现 RouteCondition 的类。

use Snicco\Component\HttpRouting\Routing\Route\Route;
use Snicco\Component\HttpRouting\Routing\RoutingConfigurator\WebRoutingConfigurator;

/**
* @var WebRoutingConfigurator $configurator 
*/
$configurator->get('route1', '/route1', InvokableController::class)
             
             ->condition(OnlyIfUserAgentIsFirefox::class)
                
             // passing arguments   
             ->condition(OnlyIfHeaderIsPresent::class, 'X-CUSTOM-HEADER');

路由分组

可以使用路由组将具有类似属性的路线分组在一起。

以下属性目前可以以某种形式分组

  • 中介:将合并为组中的所有路由。
  • URL 前缀:将添加到组中的所有 URL 模式。
  • 路由名称:将连接到所有路由。
  • 命名空间:将应用于所有路由,但可以在每个路由的基础上覆盖。

支持嵌套路由组。

use Snicco\Component\HttpRouting\Routing\RoutingConfigurator\WebRoutingConfigurator;

/**
* @var WebRoutingConfigurator $configurator 
*/
$configurator
    ->name('users')
    ->prefix('/base/users')
    ->middleware('auth')
    ->namespace('App\\Http\\Controllers')
    ->group(function (WebRoutingConfigurator $configurator) {
        
        // The route name will be users.profile
        // The route pattern will be /base/users/profile/{user_id}
        // The controller definition will be [App\\Http\\Controllers\\ProfileController::class, 'index']
        // The middleware is [auth, auth-confirmed] 
        $configurator->get('profile', '/profile/{user_id}', 'ProfileController@index')
                     ->middleware('auth-confirmed');
            
        $configurator->/* */->group(/* */);    
    });

控制器

控制器是附加到路由的类方法。

控制器将用于将 PSR-7 服务器请求 转换为 PSR-7 响应。(稍后介绍)

现在,最重要的是如何定义控制器以及控制器中将有哪些参数可用。

namespace App\Controller;

use Snicco\Component\HttpRouting\Http\Psr7\Request;

class RouteController {
    
    public function __invoke(Request $request){
        //
    }
    
    public function withoutRequest(string $route_param){
        //
    }
        
    public function withRequestTypehint(Request $request, string $route_param){
        //
    }
        
}

// Valid ways to define a controller:

$configurator->get('route1', '/route-1', RouteController::class)

$configurator->get('route2', '/route-2/{param}', [RouteController::class, 'withoutRequest']);

$configurator->get('route3', '/route3/{param}', 'App\\Controller\\RouteController@withRequestTypehint');
// or
$configurator->namespace('App\\Controller')->get('route3', '/route3/{param}', 'RouteController@withRequestTypehint');

如果使用完全限定的类名定义控制器,则它必须有一个 __invoke 方法。

可以省略控制器,在这种情况下,将向路由添加一个回退控制器。回退控制器将始终返回一个DelegatedResponse实例,该实例可用于表示(给另一个系统)当前请求不应被处理(由您的代码)。

传递给所有控制器方法的第一个参数是Snicco\Component\HttpRouting\Http\Psr7\Request实例(如果控制器方法有此类型提示)。

捕获的路由段按顺序传递给控制器方法。方法参数名称和路由定义中的段名称不重要。

捕获的路由段始终是字符串(在FastRoute中),但为了方便,数值会被转换为整数。

路由条件也可以返回“捕获的参数”。如果一个路由有返回参数的条件,这些参数将在捕获的URL参数之后传递给控制器方法。

重定向路由

您可以直接在路由文件中配置重定向。不是定义专用控制器,所有重定向路由都将使用RedirectController来直接创建一个RedirectResponse

use Snicco\Component\HttpRouting\Routing\RoutingConfigurator\WebRoutingConfigurator;

/**
* @var WebRoutingConfigurator $configurator
*/

$configurator->redirect('/foo', '/bar', 301);

$configurator->redirectAway('/foo', 'https://external-site.com', 302);

$configurator->redirectToRoute('/foo', 'route1', 307);

视图路由

如果您只想为给定的URL返回一个简单的模板而不涉及太多逻辑,您可以使用路由配置器上的view()方法。

use Snicco\Component\HttpRouting\Routing\RoutingConfigurator\WebRoutingConfigurator;

/**
* @var WebRoutingConfigurator $configurator
*/

$configurator->view('/contact', 'contact.php');

当此路由匹配时,它将返回一个ViewResponse实例。如何将其转换为底层模板取决于您。您可能需要在自定义中间件中使用您喜欢的模板引擎来实现这一点。

管理路由

定义在admin.php文件中的路由具有特殊之处,因为它们可以用来创建指向CMS(如WordPress)管理区域的路由,通常您无法控制“路由”。

您甚至可以直接从路由定义中创建管理菜单项。

所有这些实现细节都被AdminArea接口和AdminMenu接口抽象。

通过使用AdminRoutingConfigurator来配置管理路由。

管理路由仅限于GET请求。

您将使用AdminRoutingConfigurator::page()AdminRoutingConfigurator::subPage()方法而不是使用WebRoutingConfigurator::get()方法。

以下是如何在WordPress中使用它的示例,在管理区域中使用page查询变量来进行路由。查看WPAdminArea,这是AdminArea接口在WordPress中的实现。

// .routes/admin.php
use Snicco\Component\HttpRouting\Routing\Admin\AdminMenu;
use Snicco\Component\HttpRouting\Routing\Admin\AdminMenuItem;
use Snicco\Component\HttpRouting\Routing\RoutingConfigurator\AdminRoutingConfigurator;

/**
* @var AdminRoutingConfigurator $configurator
*/

$configurator->name('my-plugin')
             ->middleware('my-plugin-middleware')
             ->group(function (AdminRoutingConfigurator $configurator) {
             
                $parent_route = $configurator
                   ->page('overview', '/admin.php/overview', OverViewController::class)
                   ->middleware('parent-middleware');
                
                $configurator->page(
                   'settings', 
                   '/admin.php/settings', 
                   SettingsController::class, 
                   [
                      // Explicitly configure menu item attributes.
                      AdminMenuItem::MENU_TITLE => 'Custom settings title'
                   ]
                   $parent_route // Set a parent route to create a menu hierarchy. Middleware is inherited.
                );
                
             });

在您的默认WordPress安装中,这些路由将匹配以下路径

  • /wp-adming/admin.php?page=overview
  • /wp-adming/admin.php?page=settings

根据您的路由名称和路由模式,将自动将一个 AdminMenuItem 实例添加到通过 Router 可用的 AdminMenu

API 路由

API 路由目录中的路由文件与“常规”路由之间的区别是,将使用 RouteLoadingOptions::getApiRouteAttributes() 方法为每个路由应用默认设置。

这允许例如

  • 为所有路由添加基础前缀,如 /api
  • api. 作为路由名称的前缀
  • 从文件名中解析版本号并将其作为前缀应用 /api/v1

使用 API 路由完全是可选的。

路由缓存

上述所有内容 都将在生产中缓存在一个单一的 PHP 文件中,该文件可以通过 OPcache 非常快速地返回。

出于这个原因,此包有意不支持 Closures 作为“路由控制器”。在 PHP 中无法原生序列化 Closures

在内部,FastRoute 只包含每个路由的名称。一旦匹配到某个路由,将仅对该单个路由进行初始化并“运行”。

随着您的应用程序中路由数量的增加,这提供了显著的性能提升。

请查看 SerializedRouteCollection 以获取详细信息。

匹配路由

Router::urlMatcher() 的第一次调用将延迟加载和配置所有路由(或返回缓存的那些)。

use Snicco\Component\HttpRouting\Routing\UrlMatcher\RoutingResult;
use Snicco\Component\HttpRouting\Routing\UrlMatcher\UrlMatcher;

$router = /* */

/** @var  $url_matcher */
$url_matcher = $router->urlMatcher();

$psr_server_request = /* create any psr7 server request here. */

$routing_result = $url_matcher->dispatch($psr_server_request);

$routing_result->isMatch();
$routing_result->route();
$routing_result->decodedSegments();

反向路由/URL 生成

路由系统始终是双向的

  • URL > 路由
  • 路由名称 + 参数 > URL

FastRoute 只提供第一部分。此包填补了这一空白。

Router::urlGenerator() 的第一次调用将延迟加载和配置所有路由(或返回缓存的那些)。

在生成 URL 时考虑 正则表达式约束,如果提供的值会导致不匹配路由,将抛出异常。

use Snicco\Component\HttpRouting\Routing\Router;
use Snicco\Component\HttpRouting\Routing\UrlGenerator\UrlGenerator;

// In a route file:
$configurator->get('route1', '/route1/{param1}/{param2}', RouteController::class)
              ->requireAlpha('param1')
              ->requireNum('param2');

/**
* @var Router $router 
*/
$router = /* */

$url_generator = $router->urlGenerator();

$url = $url_generator->toRoute('route1', ['param1' => 'foo', 'param2' => '1']); 
var_dump($url); // /route1/foo/1

$url = $url_generator->toRoute('route1', ['param1' => 'foo', 'param2' => '1'], UrlGenerator::ABSOLUTE_URL); 
var_dump($url); // https://snicco.io/route1/foo/1 (host and scheme depend on your UrlGenerationContext)


// This will throw an exception because param2 is not a number
$url_generator->toRoute('route1', ['param1' => 'foo', 'param2' => 'bar']); 

管理员菜单

如果您正在使用 管理路由,则将根据您的路由定义自动配置一个 AdminMenu 实例。

您可以使用 AdminMenu 对象来配置某些外部系统或旧版 CMS(如果适用)。

Router::adminMenu() 的第一次调用将延迟加载和配置所有路由(或返回缓存的那些)。

/**
* @var Router $router 
*/
$router = /* */

$admin_menu = $router->adminMenu();

foreach ($admin_menu->items() as $menu_item) {
    // register the menu item somewhere.
}

PSR-15 中间件分发器

此包附带一个非常强大的 PSR-15 中间件分发器,该分发器已集成了配置的路由系统。

核心是 MiddlewarePipeline

创建中间件管道

中间件管道需要一个 PSR-11 容器 来延迟解析您的控制器和中间件。

此外,还需要一个 HTTPErrorHanlder 实例来处理每个中间件的异常。

use Psr\Container\ContainerInterface;
use Snicco\Component\HttpRouting\Middleware\MiddlewarePipeline;
use Snicco\Component\Psr7ErrorHandler\ProductionErrorHandler;

/**
* @var ContainerInterface $psr_11_container 
*/
$psr_11_container = /* */

/**
* @var ProductionErrorHandler 
*/
$psr7_error_handler = /* */

$pipeline = new MiddlewarePipeline(
    $psr_11_container,
    $psr7_error_handler
);

管道请求

在基本层面上,中间件管道接受一个 PSR-7 服务器请求,将其通过多个PSR-15 中间件,并返回一个PSR-7 响应。如何发送该响应对象取决于您。

use Snicco\Component\HttpRouting\Http\Psr7\Request;
use Snicco\Component\HttpRouting\Middleware\MiddlewarePipeline;

/**
* @var MiddlewarePipeline $pipeline
*/
$pipeline = /* */

$response = $pipeline
               ->send($server_request)
               ->through([
                   Psr15MiddlewareOne::class,
                   Psr15MiddlewareTwo::class,
                ])->then(function (Request $request) {
                    // Throw exception or return a default response.
                    throw new RuntimeException('Middleware pipeline exhausted without returning response.');
                });

为了将中间件管道与我们的路由系统连接,我们使用此包内建的 PSR-15 中间件。

RoutingMiddleware 负责将管道中的当前请求匹配到 路由系统 的某个路由。

use Snicco\Component\HttpRouting\Middleware\RoutingMiddleware;

$routing_middleware = new RoutingMiddleware(
    $router->urlMatcher();
);

RouteRunner 负责执行匹配的路由。

如果没有匹配到路由,将返回一个 DelegatedResponse 实例。

如果匹配到路由,将发生以下操作

  • 将解析匹配路由的所有中间件。
  • 创建一个新的(内部)中间件管道,通过所有路由中间件传输请求。
  • 内部中间件管道的最后一步将解析容器中的路由控制器并执行它。

为了实例化 RouteRunner,我们首先需要一个 MiddlewareResolver

use Snicco\Component\HttpRouting\Middleware\RouteRunner;

$pipeline = /* This can be the same pipeline we created initially. The pipeline is immutable anyway. */
$psr_11_container = /* */
$middleware_resolver = /* */

$route_runner = new RouteRunner($pipeline, $middleware_resolver, $psr_11_container);

$response = $pipeline->send($server_request)
                     ->through([
                        $routing_middleware,
                        $route_runner   
                     ])->then(function () {
                        throw new RuntimeException('Middleware pipeline exhausted.');
                     });

MiddlewareResolver

正如类名所示,MiddlewareResolver 负责解析应用于单个路由和/或请求的所有中间件。

use Snicco\Component\HttpRouting\Middleware\MiddlewareResolver;

// The following four middleware groups can be set to always be applied, even if no route matched.
$always_run = [
    'global'
    'frontend',
    'admin',
    'api'
]

// This configures the short aliases we used in our route definitions
$middleware_aliases = [
    'auth' => AuthenticateMiddleware::class
]

// An alias can also be a middleware group.
// Middleware groups can contain other groups.
$middleware_groups = [
    'group1' => [
        'auth', // group contains alias
        SomePsr15Middleware::class    
    ],
    'group2' => [
        'group1,' // fully contains group1
         SomeOtherPsr15Middleware::class    
    ],
    'global' => [],
    'frontend' => [ ],
    'api' => [
        RateLimitAPI::class    
    ],
    'admin' => []   
];

// A list of class names, the 0-index has the highest priority, meaning that it will
// always run first.
$middleware_priority = [
    SomePsr15Middleware::class,
    SomeOtherPsr15Middleware::class
];

$middleware_resolver = new MiddlewareResolver(
    $always_run,
    $middleware_aliases, 
    $middleware_groups,
    $middleware_priority
);

中间件解析器可以被缓存以最大化性能。

缓存中间件解析器意味着,对于您的应用程序中的每个路由,中间件都已经递归解析,分组已展开,别名已解析等。

use Snicco\Component\HttpRouting\Middleware\MiddlewareResolver;

$middleware_resolver = new MiddlewareResolver();

$store_me = $middleware_resolver->createMiddlewareCache(
    $router->routes(),
    $psr_11_container
);

file_put_contents('/path/to/cache-dir/middleware-cache.php', '<?php return ' . var_export($store_me, true) . ';');

list($route_map, $request_map) = require '/path/to/cache-dir/middleware-cache.php';

$cached_resolver = MiddlewareResolver::fromCache($route_map, $request_map);

PSR 工具

此包包含一些扩展 PSR 接口以提供一些实用辅助器的类。

使用它们完全是可选的

  • 抽象的 Middleware 和抽象的 Controller 可以被扩展。它们都提供了对 ResponseUtils 类的访问,并包含对 URLGenerator 的引用。
  • Request 类包装了任何 PSR-7 请求,并提供了一些在 PSR-7 接口中未定义的有用方法。
  • Response 类包装了任何 PSR-7 响应,并提供了一些在 PSR-7 接口中未定义的有用方法。

贡献

此存储库是 Snicco 项目 开发存储库的只读分割。

以下是如何进行贡献.

报告问题和发送拉取请求

请在 Snicco monorepo 中报告问题。

安全性

如果您发现安全漏洞,请遵循我们的 披露程序