makise-co/http-router

PSR HTTP 路由器

v1.0.4 2020-10-25 00:00 UTC

This package is auto-updated.

Last update: 2024-09-25 09:10:12 UTC


README

基于FastRoute的PSR HTTP路由器,支持中间件

需求

  • PHP >= 7.4

安装

composer require makise-co/http-router

特性

  • 中间件
  • 依赖注入到路由处理程序(通过PSR容器和php-di/invoker
  • 支持大多数常见的请求处理程序声明(闭包、可调用、数组可调用、类@方法、类::方法)
  • 遵循严格的PSR标准(PSR-7和PSR-15),每个路由处理程序必须返回PSR ResponseInterface的实例
  • 高度可移植性和可定制性(任何实现部分都可以替换为您自己的实现)

常见问题解答

  • 如何获取正在处理请求的路由实例?

    $route = $request->getAttribute(RouteInterface::class);
  • 如何获取路由参数?

    // method 1 - Using DependencyInjection, many frameworks provides this way
    function apiEndpoint(int $id): \Psr\Http\Message\ResponseInterface
    {
    }
    
    // method 2 - Manually getting route arguments
    function apiEndpoint(\Psr\Http\Message\ServerRequestInterface $request): \Psr\Http\Message\ResponseInterface
    {
        $args = $request->getAttribute(Router::ROUTE_ARGS);
    }

性能

对于三百万次调用(使用php-di/invoker)

Time took: 4.95980096 secs (0.00000165 secs per request)

对于三百万次调用(使用原生函数调用实现,不使用DI)

Time took: 3.31491995 secs (0.00000110 secs per request)

基准代码可以在这里找到。

  • 基准测试在PHP 7.4和开启OPcache的情况下进行
  • CPU: Intel Core i7-9750H 6核(基准测试时的CPU频率:4.07 GHz)
  • 操作系统: Ubuntu 20.04(WSL 2)

用法

<?php

declare(strict_types=1);

use DI\Container; // Any PSR container implementation
use MakiseCo\Http\Router\RouteCollectorFactory;
use MakiseCo\Http\Router\RouteCollectorInterface;

$collector = (new RouteCollectorFactory())->create(
    // container that is used to resolve route handlers
    // and inject dependencies into route handlers
    new Container(),
);

$collector->get('/', function (): Response {
    return new Response(200, [], 'Welcome');
});

$collector->addGroup(
    'api',
    [
        'namespace' => 'App\\Http\\Controllers\\',
        'middleware' => [AddHeaderMiddleware1::class]
    ],
    function (RouteCollectorInterface $collector) {
        $collector
            ->get('/balance', function (): Response {
                return new Response(200, [], '228');
            })
            ->setName('api_balance')
            ->withMiddleware(AddHeaderMiddleware2::class)
            // or
            ->withMiddleware(new AddHeaderMiddleware2())
            ->withAttribute('auth_permissions', ['customer']);

        $collector->post('/posts', function (ServerRequestInterface $request): Response {
            $body = $request->getParsedBody();

            return new Response(200, [], json_encode($body));
        });

        $collector->get('/posts/{id:\d+}', function (int $id): Response {
            return new Response(200, [], "Post {$id}");
        });

        $collector->get('/user', 'UserController@index');
        // or
        $collector->get('/user', [$userController, 'index']);
        // or
        $collector->get('/user', 'UserController::index');
        // or
        $collector->get('/user', [UserController::class, 'index']);
    }
);

$router = $collector->getRouter();

$request = new ServerRequest('GET', '/api/balance');
$response = $router->handle($request);

延迟解析

当您使用控制器方法编写路由处理程序时,您可能希望将一些依赖项注入到控制器构造函数中。默认情况下,所有依赖项将在您注册路由后自动注入到您的控制器中。

这种行为可能导致您的应用程序中出现各种错误或错误。让我们想象一下您的控制器类看起来像这样

class PostController
{
    private ORMInterface $orm;

    public function __construct(ORMInterface $orm)
    {
        $this->orm = $orm;   
    }
    
    public function index(): Response
    {
        $rows = $this
            ->orm
            ->getRepository(\App\Entity\Post::class)
            ->findAll();

        return new Response(json_encode($rows));
    }
}

然后您为PostController的索引方法注册一个路由

$collector->get('/posts', 'PostController@index');

之后,您的PostController类的实例将被立即创建,并且将向其中注入ORM实例。ORM可以初始化数据库连接并查询数据库模式信息。

在多进程应用程序架构(当您的应用程序具有主进程和许多工作进程时)中,这可能导致不必要的资源消耗(耗尽数据库连接、内存消耗、较慢的应用程序启动时间)。
它甚至可能导致应用程序中的错误,特别是与Swoole后端一起使用时,因为所有非阻塞I/O操作必须在高并发上下文中执行。

此包提供了一个简单的解决方案来避免所有这些问题:延迟路由处理程序解析。只需使用RouteCollectorLazyFactory而不是RouteCollectorFactory

而不是立即解析路由处理程序,这个过程将被推迟到路由编译阶段。
路由的编译阶段发生在您调用“RouteCollector”实例上的“getRouter”方法时。
这意味着路由处理程序是在调用“getRouter”方法时解析的。

错误处理

<?php

declare(strict_types=1);

use GuzzleHttp\Psr7\Response;
use MakiseCo\Http\Router\Exception\MethodNotAllowedException;
use MakiseCo\Http\Router\Exception\RouteNotFoundException;
use MakiseCo\Middleware\ErrorHandlerInterface;
use MakiseCo\Middleware\ErrorHandlingMiddleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

// It is just an example implementation (a more proper way is to use a response factory)
class ErrorHandler implements ErrorHandlerInterface
{
    public function handle(Throwable $e, ServerRequestInterface $request): ResponseInterface
    {
        if ($e instanceof RouteNotFoundException) {
            return new Response(404, [], $e->getMessage());
        }

        if ($e instanceof MethodNotAllowedException) {
            // following https://tools.ietf.org/html/rfc7231#section-6.5.5
            return new Response(405, ['Allow' => $e->getAllowedMethods()], $e->getMessage());
        }

        return new Response(500, [], "Internal Server Error<br><br>{$e->getMessage()}");
    }
}

$router = $collector->getRouter();
$app = (new \MakiseCo\Middleware\MiddlewarePipeFactory())->create([
    new ErrorHandlingMiddleware(new ErrorHandler()), // placing error handling middleware first
    $router
]);
// or
$app = new \MakiseCo\Middleware\Dispatcher([
    new ErrorHandlingMiddleware(new ErrorHandler()), // placing error handling middleware first
    $router
]);
// or use any other middleware dispatcher

$response = $app->handle($request);

更完整的示例可以在这里找到。