woohoolabs/harmony

Woohoo Labs. Harmony

8.0.0 2023-08-19 21:35 UTC

README

Latest Version on Packagist Software License Build Status Coverage Status Quality Score Total Downloads Gitter

Woohoo Labs. Harmony 是一个 PSR-15 兼容的中间件调度器。

Harmony 是为了成为一个完全灵活且几乎不可见的框架而诞生的。这就是为什么 Harmony 支持 PSR-7PSR-11 以及 PSR-15 标准。

目录

介绍

理由

这篇博客文章最好地解释了为什么 Harmony 在 2014 年开始:[http://www.catonmat.net/blog/frameworks-dont-make-sense/](http://www.catonmat.net/blog/frameworks-dont-make-sense/)

特性

  • 由于 Harmony 的简单性,性能高
  • 由于 PSR-15 的庞大中间件生态系统,灵活性高
  • 通过 PSR-7 完全控制 HTTP 消息
  • 通过 PSR-11(以前称为 Container-Interop)支持许多 DI 容器

为什么选择 Harmony?

现在有许多非常相似的中间件调度器库,如 Laminas-StratigilitySlim Framework 3Relay。你可能会问自己,为什么还需要另一个具有相同功能的库?

我们认为 Harmony 提供了两个关键特性,证明了其存在的合理性:

  • 它是所有库中最简单的。虽然简单是主观的,但有一点是肯定的:Harmony 提供了此类库所需的最基本功能。这就是为什么 Harmony 本身只包含 ~140 行的单个类。

  • 从版本 3 开始,Harmony 原生支持 条件 的概念,这对于中间件调度器来说是一个罕见的功能。这使得处理面向中间件方法的一个主要弱点(即条件调用中间件的能力)变得更加容易。

用例

当然,Harmony 并不适合所有项目和团队:这个框架最适合经验丰富的团队和长期项目。对于经验不足的团队——尤其是如果他们有短期截止日期——可能需要选择一个具有更多功能、开箱即用的框架,以加快其初始阶段的发展。当您的软件需要长期支持时,Harmony 的灵活性最有优势。

概念

Woohoo Labs. Harmony 建立在两个主要概念之上:中间件,它促进了关注点的分离,以及通用接口,这使得依赖于松散耦合的组件成为可能。

通过使用中间件,您可以轻松地掌握请求-响应生命周期的流程:您可以在路由之前进行身份验证,在响应发送后进行一些日志记录,或者甚至在一个请求中调度多个路由。这一切都可以实现,因为 Harmony 中的所有内容都是中间件,因此框架本身只包含约 140 行代码。这就是为什么没有框架级别的配置,只有中间件可以进行配置。您如何使用 Harmony 取决于您的想象和需求。

但是中间件必须协同工作(路由器和分发器彼此紧密耦合)。这就是为什么为框架的不同组件提供公共接口也很重要的原因。

自然地,我们决定使用PSR-7来模拟HTTP请求和响应。为了方便使用不同的DI容器,我们采用了PSR-11(原名Container-Interop),这是各种容器开箱即用的。

中间件接口设计

Woohoo Labs。Harmony的中间件接口设计基于PSR-15事实标准。

如果您想了解这种风格的详细信息,请参阅以下文章,这些文章描述了该概念本身

安装

开始之前,您只需要Composer

安装PSR-7实现

因为Harmony需要PSR-7实现(一个提供psr/http-message-implementation虚拟包的包),您必须先安装一个。您可以使用Laminas Diactoros或任何其他您喜欢的库

$ composer require laminas/laminas-diactoros

安装Harmony

要安装此库的最新版本,请运行以下命令

$ composer require woohoolabs/harmony

注意:默认情况下不会下载测试和示例。如果您需要它们,必须使用composer require woohoolabs/harmony --prefer-source或克隆存储库。

Harmony 6.2+至少需要PHP 7.4,但您可以使用Harmony 6.1来支持PHP 7.1+。

安装可选依赖

如果您想使用默认的中件间堆栈,那么您还必须要求以下依赖项

$ composer require nikic/fast-route # FastRouteMiddleware needs it
$ composer require laminas/laminas-httphandlerrunner # LaminasEmitterMiddleware needs it

基本用法

定义您的端点

以下示例仅适用于您使用默认分发器中间件的情况。这里有两个需要注意的重要事项:首先,每个可调用的端点都接收一个Psr\Http\Message\ServerRequestInterface和一个Psr\Http\Message\ResponseInterface对象作为参数,并且后者应该被操作并返回。其次,您不仅可以使用类方法作为端点,还可以定义其他可调用的对象(请参阅以下路由部分)。

namespace App\Controllers;

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

class UserController
{
    public function getUsers(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $users = ["Steve", "Arnie", "Jason", "Bud"];
        $response->getBody()->write(json_encode($users));

        return $response;
    }

    public function updateUser(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $userId = $request->getAttribute("id");
        $userData = $request->getParsedBody();

        // Updating user...

        return $response;
    }
}

定义您的路由

以下示例仅适用于您使用基于FastRoute默认路由器中间件。我们选择默认使用它是因为它的性能和简单性。您可以在Nikita的博客中了解更多信息。

让我们将上述端点的路由添加到FastRoute中

use App\Controllers\UserController;

$router = FastRoute\simpleDispatcher(function (FastRoute\RouteCollector $r) {
    // An anonymous function endpoint
    $r->addRoute("GET", "/me", function (ServerRequestInterface $request, ResponseInterface $response) {
            // ...
    });

    // Class method endpoints
    $r->addRoute("GET", "/users", [UserController::class, "getUsers"]);
    $r->addRoute("POST", "/users/{id}", [UserController::class, "updateUser"]);
});

最后,启动应用程序

use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use WoohooLabs\Harmony\Harmony;
use WoohooLabs\Harmony\Middleware\DispatcherMiddleware;
use WoohooLabs\Harmony\Middleware\FastRouteMiddleware;
use WoohooLabs\Harmony\Middleware\LaminasEmitterMiddleware;

$harmony = new Harmony(ServerRequestFactory::fromGlobals(), new Response());
$harmony
    ->addMiddleware(new LaminasEmitterMiddleware(new SapiEmitter()))
    ->addMiddleware(new FastRouteMiddleware($router))
    ->addMiddleware(new DispatcherMiddleware())
    ->run();

为了使框架正常运行,您必须注册所有之前的中间件

  • LaminasEmitterMiddleware通过laminas-httphandlerrunner将响应发送到ether
  • FastRouteMiddleware负责路由(在上一步骤中配置了$router
  • DispatcherMiddleware分发属于请求当前路由的控制器

注意,Harmony::addMiddleware()有一个可选的第二个参数,可以定义中间件的ID(如果您想在代码中的某个地方调用Harmony::getMiddleware(),则必须这样做)。

当然,您完全可以根据自己的需要添加额外的中间件或用您自己的实现替换它们。当您想上线时,请调用$harmony->run()

高级用法

使用可调用的类控制器

大多数情况下,您会将端点(~控制器操作)定义为常规的可调用函数,如默认路由部分的示例所示。

$router->addRoute("GET", "/users/me", [\App\Controllers\UserController::class, "getMe"]);

如今,只包含一个操作的控制器越来越受欢迎。在这种情况下,实现__invoke()魔术方法是普遍的做法。遵循这种思路,您的路由定义可以简化,如下所示:

$router->addRoute("GET", "/users/me", \App\Controllers\GetMe::class);

注意:如果您使用的是默认路由器或分发器之外的其他路由器或分发器,请确保该功能对您可用。

如果您对如何在Action-Domain-Responder模式中从可调用控制器中获益感兴趣,您可以在Paul M. Jones的博客文章中找到一个有见地的描述。

使用Harmony与您喜欢的依赖注入(DI)容器一起使用

Woohoo Labs创建Harmony的动机是能够改变框架的每一个方面。这就是为什么您可以使用任何想要的DI容器。

为此,我们选择在内置的DispatcherMiddleware上构建基于PSR-11 - 最广泛使用的通用DI容器接口。

重要的是要知道,DispatcherMiddleware默认使用BasicContainer。它不过是一个非常愚蠢的DIC,它尝试根据类名创建对象(因此调用$basicContainer->get(Foo::class)将创建一个新的Foo实例)。

但如果你向中间件的构造函数提供参数,你也可以使用你喜欢的符合PSR-11的DI容器。让我们看看一个例子,其中有人想用Zen替换BasicContainer

$container = new MyContainer();
$harmony->addMiddleware(new DispatcherMiddleware($container));

创建自定义中间件

新的中间件也需要实现PSR-15MiddlewareInterface。让我们看看一个示例。

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;

class LoggerMiddleware implements MiddlewareInterface
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // Perform logging before handling the request
        $this->logger->info("Request needs to be handled");

        // Invoking the remaining middleware
        $response = $handler->handle($request);

        // Perform logging after the request has been handled
        $this->logger->info("Request was successfully handled");

        // Return the response
        return $response;
    }
}

当您准备好后,将其附加到Harmony。

$harmony->addMiddleware(new LoggerMiddleware(new Logger()));

如果您不想调用剩余的中间件(可能是因为错误),则可以简单地操作并返回一个响应,其“原型”已在其构造函数中传递给中间件。您可以在以下示例中看到这一点。

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class AuthenticationMiddleware implements MiddlewareInterface
{
    protected string $apiKey;
    protected ResponseInterface $errorResponsePrototype;

    public function __construct(string $apiKey, ResponseInterface $errorResponsePrototype)
    {
        $this->apiKey = $apiKey;
        $this->errorResponsePrototype = $errorResponsePrototype;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // Return Error 401 "Unauthorized" if the provided API key doesn't match the expected one
        if ($request->getHeader("x-api-key") !== [$this->apiKey]) {
            return $this->errorResponsePrototype->withStatus(401);
        }

        // Invoke the remaining middleware if authentication was successful
        return $handler->handle($request);
    }
}

然后

$harmony->addMiddleware(new AuthenticationMiddleware("123"), new Response());

定义条件

非平凡的应用程序在执行其中间件管道时通常需要某种形式的分支。一个可能的用例是在它们只想对其某些端点执行身份验证或当它们想检查CSRF令牌(如果请求方法是POST)时。Harmony 2也使分支易于处理,但Harmony 3+帮助您优化中间件中的条件逻辑性能。

让我们回顾一下上一节中的身份验证中间件示例!这次,我们只想对位于/users路径下的端点进行身份验证。在Harmony 2中,我们必须用类似以下的方式实现它:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class AuthenticationMiddleware implements MiddlewareInterface
{
    protected string $securedPath;
    protected MyAuthenticatorInterface $authenticator;
    protected ResponseInterface $errorResponsePrototype;

    public function __construct(string $securedPath, MyAuthenticatorInterface $authenticator, ResponseInterface $errorResponsePrototype)
    {
        $this->securedPath = $securedPath;
        $this->authenticator = $authenticator;
        $this->errorResponsePrototype = $errorResponsePrototype;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // Invoke the remaining middleware and cancel authentication if the current URL is for a public endpoint
        if (substr($request->getUri()->getPath(), 0, strlen($this->securedPath)) !== $this->securedPath) {
            return $handler->handle($request);
        }

        // Return Error 401 "Unauthorized" if authentication fails
        if ($this->authenticator->authenticate($request) === false) {
            return $this->errorResponsePrototype->withStatusCode(401);
        }

        // Invoke the remaining middleware otherwise
        return $handler->handle($request);
    }
}

最后,将中间件附加到Harmony。

$harmony->addMiddleware(new AuthenticationMiddleware("/users", new ApiKeyAuthenticator("123"), new Response()));

您必须在中间件内部检查当前URI,这样问题就解决了。这样做的不利之处在于,即使不需要身份验证,AuthenticationMiddleware及其所有依赖项也会在每个请求中实例化!如果您依赖于复杂对象图,这可能非常不便。

然而,在Harmony 3+中,您可以使用条件来优化要调用的中间件数量。在这种情况下,您可以使用内置的PathPrefixCondition。您只需要将其附加到Harmony。

$harmony->addCondition(
    new PathPrefixCondition(["/users"]),
    static function (Harmony $harmony) {
        $harmony->addMiddleware(new AuthenticationMiddleware(new ApiKeyAuthenticator("123")));
    }
);

这样,只有当 PathPrefixCondition 评估为 true 时(当前 URI 路径以 /users 开头时),AuthenticationMiddleware 才会被实例化。此外,你还可以在匿名函数中附加更多的中间件到 Harmony。它们将一起执行,就像它们是包含中间件的一部分。

以下是内置条件的完整列表

  • ExactPathCondition:如果当前 URI 路径与允许的任何路径完全匹配,则评估为 true。

  • PathPrefixCondition:如果当前 URI 路径以允许的任何路径前缀开始,则评估为 true。

  • HttpMethodCondition:如果当前 HTTP 方法与允许的任何 HTTP 方法匹配,则评估为 true。

示例

如果你想看到一个非常基本的实际应用结构,请查看 示例。如果您的系统上提供了 docker-composemake,则可以按照以下顺序运行以下命令来尝试示例应用程序

cp .env.dist .env      # You can now edit the settings in the .env file
make composer-install  # Install the Composer dependencies
make up                # Start the webserver

如果您没有 make,您可以复制底层命令,并在终端中直接使用它们。

最后,示例应用程序在 localhost:8080 上可用。

如果您修改了 .env 文件,您应该将端口号更改为 HOST_WEB_PORT 变量的值。

示例 URI

  • GET /books/1
  • GET /users/1
  • GET /me

完成工作后,只需停止 web 服务器

make down

如果前提条件对您不可用,您必须在您的宿主机上设置一个 web 服务器,安装 PHP,以及通过 Composer 安装依赖项。

版本控制

此库遵循 SemVer v2.0.0

变更日志

有关最近更改的更多信息,请参阅 更改日志

测试

Harmony 有一个 PHPUnit 测试套件。要从项目文件夹中运行测试,请运行以下命令

$ phpunit

此外,您还可以运行 docker-compose upmake test 来执行测试。

贡献

有关详细信息,请参阅 贡献指南

支持

有关详细信息,请参阅 支持

鸣谢

许可证

MIT 许可证 (MIT)。有关更多信息,请参阅 许可文件