phly/conduit

此包已被放弃,不再维护。作者建议使用 zendframework/zend-stratigility 包。

PHP 中间件

0.15.2 2015-05-21 13:43 UTC

README

已放弃!或者,更确切地说,已经重新命名!

phly/conduit 已迁移到 zendframework 组织,作为 zend-stratigility(从 Strata,意为“层”,和“敏捷”),

请使用该包,并对它提交问题和拉取请求。在此期间,我已关闭 phly/conduit 的问题和拉取请求。

Scrutinizer Code Quality Code Coverage Scrutinizer Build Status

Conduit 是将 Sencha Connect 移植到 PHP 的项目。它允许您通过中间件构建应用程序。

安装和需求

使用 composer 安装此库

$ composer require phly/http phly/conduit

Conduit 有以下依赖项(由 Composer 管理)

  • psr/http-message,它提供了在 PSR-7 中指定的接口,并在本包中进行了类型提示。为了使用 Conduit,您需要一个 PSR-7 的实现;其中一个这样的包是 phly/http(因此在上面的安装行中对其进行了引用)。
  • zendframework/zend-escaper,用于在将错误消息传递给响应之前对其进行转义。

如果您希望提供自己的请求和响应实现,只要它们实现了 PSR HTTP 消息接口即可。

使用

创建应用程序包括 3 个步骤

  • 创建中间件或中间件管道
  • 使用中间件创建服务器
  • 指示服务器监听请求
use Phly\Conduit\MiddlewarePipe;
use Phly\Http\Server;

require __DIR__ . '/../vendor/autoload.php';

$app    = new MiddlewarePipe();
$server = Server::createServer($app,
  $_SERVER,
  $_GET,
  $_POST,
  $_COOKIE,
  $_FILES
);
$server->listen();

上述示例本身是无用的,直到您将中间件管道连接到应用程序。

中间件

什么是中间件?

中间件是存在于请求和响应之间的代码,它可以接收传入的请求,根据它执行操作,然后完成响应或将代理传递给队列中的下一个中间件。

use Phly\Conduit\MiddlewarePipe;
use Phly\Http\Server;

require __DIR__ . '/../vendor/autoload.php';

$app    = new MiddlewarePipe();
$server = Server::createServer($app, $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES);

// Landing page
$app->pipe('/', function ($req, $res, $next) {
    if ($req->getUri()->getPath() !== '/') {
        return $next($req, $res);
    }
    return $res->end('Hello world!');
});

// Another page
$app->pipe('/foo', function ($req, $res, $next) {
    return $res->end('FOO!');
});

$server->listen();

在上面的示例中,我们有两个中间件的示例。第一个是一个着陆页,监听路径 /。如果路径完全匹配,则完成响应。如果不匹配,则委托给堆栈中的下一个中间件。第二个中间件匹配路径 /foo -- 意味着它会匹配 /foo/foo/ 和任何子路径。在这种情况下,它将使用自己的消息完成响应。如果此时没有匹配的路径,则默认组成“最终处理器”报告 404 状态。

因此,简要来说,中间件是接受请求和响应对象并对其进行处理的 PHP 可调用函数

中间件可以通过调用传递的第三个参数 $next 可调用函数来决定执行更多处理。在这种范式下,您可以构建一个用于处理请求的工作流引擎——例如,您可以设置中间件执行以下操作:

  • 处理身份验证细节
  • 执行内容协商
  • 执行HTTP协商
  • 将路径路由到更合适、更具体的处理器

每个中间件本身也可以是中间件,并且可以附加到特定的路径——允许您在公共域名下混合和匹配应用程序。例如,您可以将API中间件放在提供其文档的中间件旁边,放在提供文件的中间件旁边,并通过URI将每个中间件隔离开来。

$app->pipe('/api', $apiMiddleware);
$app->pipe('/docs', $apiDocMiddleware);
$app->pipe('/files', $filesMiddleware);

以这种方式附加到每个中间件的处理程序将看到带有该路径段的路由URI——允许它们单独开发并在任何路径下重用。

在Conduit中,中间件可以是:

  • 任何接受最小化PSR-7请求和响应(按此顺序),以及可选的可调用函数(用于调用队列中的下一个中间件,如果有的话)的PHP可调用函数。PSR-7
  • 实现 Phly\Conduit\MiddlewareInterface 的对象。Phly\Conduit\MiddlewarePipe 实现了这个接口。

错误处理器

要处理错误,您可以编写接受 正好 四个参数的中间件

function ($error, $request, $response, $next) { }

或者,您可以实现 Phly\Conduit\ErrorMiddlewareInterface

当使用 MiddlewarePipe 时,在队列执行过程中,如果调用 $next() 时带有参数或抛出异常,中间件将遍历队列直到找到第一个此类错误处理器。该错误处理器可以完成请求,或者自己调用 $next()调用 $next() 的错误处理器应使用它自己接收到的错误,或使用另一个错误。

错误处理器通常附加到中间件的末尾,以防止尝试执行非错误处理中间件,并确保它们可以拦截来自任何其他处理器的错误。

创建中间件

要创建中间件,编写一个可调用函数,该函数可以接收最小化的请求和响应对象,以及可选的调用链中下一个的回调函数。在您的中间件中,您可以处理请求的任何部分——包括委托给其他中间件。如果您的中间件接受第三个参数 $next,如果它无法完成请求或允许进一步处理,它可以调用它来返回给父中间件的处理。

以下是一个示例中间件,它将使用外部路由器将传入请求路径映射到处理器;如果无法映射请求,它将返回给下一个中间件的处理。

function ($req, $res, $next) use ($router) {
    $path = $req->getUri()->getPath();

    // Route the path
    $route = $router->route($path);
    if (! $route) {
        return $next($req, $res);
    }

    $handler = $route->getHandler();
    return $handler($req, $res, $next);
}

以这种方式编写的中间件可以是以下任何一种:

  • 闭包(如上所示)
  • 函数
  • 静态类方法
  • PHP数组回调(例如,[ $dispatcher, 'dispatch' ],其中 $dispatcher 是一个类实例)
  • 可调用的PHP对象(即实现 __invoke() 的类的实例)
  • 实现 Phly\Conduit\MiddlewareInterface 的对象(包括 Phly\Conduit\MiddlewarePipe

在所有情况下,如果您想实现类型提示,签名是

function (
    Psr\Http\Message\ServerRequestInterface $request,
    Psr\Http\Message\ResponseInterface $response,
    callable $next = null
) {
}

Conduit提供的实现还允许您编写专门的错误处理中间件。签名与正常中间件相同,但它在签名中预置了一个额外的参数 $error。(或者,您可以实现 Phly\Conduit\ErrorMiddlewareInterface。)签名是

function (
    $error, // Can be any type
    Psr\Http\Message\ServerRequestInterface $request,
    Psr\Http\Message\ResponseInterface $response,
    callable $next
) {
}

执行和组合中间件

执行中间件最简单的方法是编写闭包并将它们附加到 Phly\Conduit\MiddlewarePipe 实例。您可以通过嵌套 MiddlewarePipe 实例来创建相关中间件的组,并使用基本路径将它们附加到其中,以便仅在匹配该路径时执行。

$api = new MiddlewarePipe();  // API middleware collection
$api->pipe(/* ... */);        // repeat as necessary

$app = new MiddlewarePipe();  // Middleware representing the application
$app->pipe('/api', $api);     // API middleware attached to the path "/api"

另一种方法是扩展 Phly\Conduit\MiddlewarePipe 类本身——尤其是如果您想允许将其他中间件附加到自己的中间件上。在这种情况下,您通常会重写 __invoke() 方法以执行任何额外的逻辑,然后调用父类以遍历中间件堆栈。

use Phly\Conduit\MiddlewarePipe;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;

class CustomMiddleware extends MiddlewarePipe
{
    public function __invoke(Request $request, Response $response, callable $next = null)
    {
        // perform some work...

        // delegate to parent
        parent::__invoke($request, $response, $next);

        // maybe do more work?
    }
}

使用此方法的另一种方法是在构造函数中重写以添加特定中间件,可能使用配置提供。在这种情况下,请确保也调用 parent::__construct() 以确保中间件队列已初始化;我建议将此作为方法的第一项行动。

use Phly\Conduit\MiddlewarePipe;

class CustomMiddleware extends MiddlewarePipe
{
    public function __construct($configuration)
    {
        parent::__construct();

        // do something with configuration ...

        // attach some middleware ...

        $this->pipe(/* some middleware */);
    }
}

这些方法特别适合于您可能希望使用现有中间件为应用程序段实现特定工作流程,但并不一定希望将该中间件应用于应用程序中的所有请求的情况。

API

以下构成了 Conduit 的主要 API。

中间件

Phly\Conduit\MiddlewarePipe 是主要的应用程序接口,之前已经讨论过。其 API 是

class MiddlewarePipe implements MiddlewareInterface
{
    public function pipe($path, $middleware = null);
    public function __invoke(
        Psr\Http\Message\ServerRequestInterface $request = null,
        Psr\Http\Message\ResponseInterface $response = null,
        callable $out = null
    );
}

pipe() 接受最多两个参数。如果只提供一个参数,则 $middleware 将分配该值,而 $path 将重新分配到值 /;这表示 $middleware 应该对任何路径进行调用。如果提供 $path,则 $middleware 将仅对该路径及其子路径执行。

中间件的执行顺序与将其管道化到 MiddlewarePipe 实例的顺序相同。

__invoke() 本身就是中间件。如果没有提供 $out,则将创建一个 Phly\Conduit\FinalHandler 实例,并在管道堆栈耗尽时使用它。可调用项应使用与 Next() 相同的签名。

function (
    Psr\Http\Message\ServerRequestInterface $request,
    Psr\Http\Message\ResponseInterface $response,
    $err = null
) {
}

内部,MiddlewarePipe 创建一个 Phly\Conduit\Next 实例,将其队列馈入其中,执行它,并返回一个响应。

Next

Phly\Conduit\Next 是中间件的主要实现细节,存在是为了允许将中间件堆栈中注册较晚的中间件进行委托。

由于 Psr\Http\Message 的接口是不可变的,如果您对请求和/或响应实例进行更改,将会有新的实例,并且您需要将这些实例告知链中的下一个中间件。 Next 预期每次调用都提供这些参数。另外,如果发生错误条件,您可以传递一个可选的第三个参数,即表示错误条件的 $err

class Next
{
    public function __invoke(
        Psr\Http\Message\ServerRequestInterface $request,
        Psr\Http\Message\ResponseInterface $response,
        $err = null
    );
}

您应该在应用程序中调用 $next() 时始终捕获或返回其返回值。期望的返回值是响应实例,但如果它不是,您可能希望返回提供的响应。

例如

提供修改后的请求

function ($request, $response, $next) use ($bodyParser)
{
    $bodyParams = $bodyParser($request);
    return $next(
        $request->withBodyParams($bodyParams), // Next will pass the new
        $response                              // request instance
    );
}

提供修改后的响应

function ($request, $response, $next)
{
    $updated = $response->addHeader('Cache-Control', [
        'public',
        'max-age=18600',
        's-maxage=18600',
    ]);
    return $next(
        $request,
        $updated
    );
}

提供修改后的请求和响应

function ($request, $response, $next) use ($bodyParser)
{
    $updated = $response->addHeader('Cache-Control', [
        'public',
        'max-age=18600',
        's-maxage=18600',
    ]);
    return $next(
        $request->withBodyParams($bodyParser($request)),
        $updated
    );
}

返回响应以完成请求

如果您对响应没有进行修改,并且不希望在管道中执行更多的中间件,请不要调用 $next() 并直接从您的中间件返回。然而,几乎总是更好的做法是返回响应实例,因为这将确保它传播回所有调用者。

function ($request, $response, $next)
{
    $response = $response->addHeader('Cache-Control', [
        'public',
        'max-age=18600',
        's-maxage=18600',
    ]);
    return $response;
}

有一个注意事项:如果您处于嵌套中间件或不是堆栈中的第一个中间件,所有父级和/或之前的中间件也必须调用 return $next(/* ... */) 以正确工作。

因此,我建议在您的中间件中调用 $next() 时始终返回

return $next(/* ... */);

并且,如果不调用 $next(),则返回响应实例

return $response

引发错误条件

要引发错误条件,将非空值作为 $next() 的第三个参数传递

function ($request, $response, $next)
{
    try {
        // try some operation...
    } catch (Exception $e) {
        return $next($request, $response, $e); // Next registered error middleware will be invoked
    }
}

FinalHandler

Phly\Conduit\FinalHandler 是在堆栈耗尽时执行的中间件的默认实现。它在被调用时期望三个参数:一个请求实例、一个响应实例和一个错误条件(如果没有错误则为 null)。它返回一个响应。

FinalHandler 在实例化时允许一个可选参数 $options,这是一个配置自己的选项数组。这些选项目前包括

  • env,应用程序环境。如果设置为 "production",则不会提供堆栈跟踪。
  • onerror,一个在调用 FinalHandler 时传递错误时要执行的调用。调用以错误(如果没有错误则为 null)、请求和响应的顺序调用。

HTTP 消息

Phly\Conduit\Http\Request

Phly\Conduit\Http\Request 作为 Psr\Http\Message\ServerRequestInterface 实例的装饰器。主要原因是允许编写中间件,以便始终可以访问原始请求实例。

例如,考虑以下内容

$app1 = new Middleware();
$app1->pipe('/foo', $fooCallback);

$app2 = new Middleware();
$app2->pipe('/root', $app1);

$server = Server::createServer($app2 /* ... */);

在上面的示例中,如果原始传入请求的 URI 是 /root/foo,则 $fooCallback 将接收一个只有 /foo 的路径历史记录的 URI。这种做法确保中间件可以安全地嵌套,并且无论嵌套级别如何都能解析。

如果您想访问完整的 URI——例如,构建当前中间件的完全限定 URI——Phly\Conduit\Http\Request 包含一个方法 getOriginalRequest(),该方法将始终返回提供给应用程序的原始请求

function ($request, $response, $next)
{
    $location = $request->getOriginalRequest()->getUri()->getPath() . '/[:id]';
    $response = $response->setHeader('Location', $location);
    $response = $response->setStatus(302);
    return $response;
}

Phly\Conduit\Http\Response

Phly\Conduit\Http\Response 作为 Psr\Http\Message\ResponseInterface 实例的装饰器,并实现 Phly\Conduit\Http\ResponseInterface,它提供了以下便利方法

  • write(),它代理到组合响应流的 write() 方法。
  • end(),它将响应标记为完成;它可以接受一个可选参数,如果提供,则传递给 write() 方法。一旦调用 end(),响应就是不可变的。
  • isComplete() 表示是否已经调用 end()

此外,它还通过方法 getOriginalResponse() 提供对服务器创建的原始响应的访问。