我最近遇到了一个关于中间件的有趣问题:当我们在编程时从基于约定的方法转向基于契约的方法时会发生什么?
基于约定的方法通常允许鸭子打字;使用中间件,这意味着您可以编写PHP可调用对象(通常是闭包)并期望它们能够正常工作。
基于契约的方法使用接口。我想你可以看到这是怎么回事。
PSR-7中间件
当PSR-7被引入时,许多中间件微框架采用了中间件的通用签名:
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; function ( ServerRequestInterface $request, ResponseInterface $response, callable $next ) : ResponseInterface
其中$next
具有以下签名:
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; function ( ServerRequestInterface $request, ResponseInterface $response ) : ResponseInterface
这种方法意味着您可以使用闭包连接中间件,从而形成一个漂亮、简洁的编程接口:
// Examples are using zend-stratigility use Zend\Diactoros\Response\TextResponse; use Zend\Stratigility\MiddlewarePipe; $pipeline = new MiddlewarePipe(); $pipeline->pipe(function ($request, $response, callable $next) { $response = $next($request, $response); return $response->withHeader('X-ClacksOverhead', 'GNU Terry Pratchett'); }); $pipeline->pipe(function ($request, $response, callable $next) { return new TextResponse('Hello world!'); });
简单易行!
这种基于约定的方法很容易编写,因为不需要创建离散的类。您可以,但这不是绝对必要的。只需将任何可调用的PHP投入其中,即可获利。
(我会注意到一些库,例如Stratigility,至少也通过接口编写了中间件,尽管接口的实现是严格可选的。)
然而,大问题是它可能导致细微的错误:
- 如果您期望的参数比中间件调度程序提供的更多会怎样?
- 如果您期望不同的参数和/或参数会怎样中间件调度程序提供的类型?
- 如果您的中间件返回意外的内容会怎样?
从本质上讲,基于约定的方法没有类型安全性,这会导致许多微妙的、意外的运行时错误。
PSR-15中间件
提议的PSR-15(HTTP服务器中间件)不基于约定,而是提议两个接口:
namespace Interop\Http\ServerMiddleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; interface MiddlewareInterface { /** * Docblock annotations, because PHP 5.6 compatibility * * @return ResponseInterface */ public function process(ServerRequestInterface $request, DelegateInterface $delegate); } interface DelegateInterface { /** * Docblock annotations, because PHP 5.6 compatibility * * @return ResponseInterface */ public function process(ServerRequestInterface $request); }
这会导致类型安全:如果您在这些接口上进行类型提示(通常,对于中间件调度程序,您只关心MiddlewareInterface
),您知道PHP会支持您关于无效的中间件。
但是,这也意味着对于任何给定的中间件,您必须创建一个类!
好吧,这让事情变得更加困难,不是吗!
或者是吗?
匿名类
从PHP7开始,我们现在可以声明匿名类。这些类似于闭包,可以将其视为匿名函数(尽管具有更多的语义和功能!),应用于类级别。
有趣的是,PHP中的匿名类允许:
- 扩展
- 接口实现
- Trait组合
换句话说,它们的行为就像任何标准类声明一样。
让我们调整之前的管道以改用PSR-15。(我们将继续使用Stratigility,因为从版本2开始,它支持提议的PSR-15规范。)
use Interop\Http\ServerMiddleware\DelegateInterface; use Interop\Http\ServerMiddleware\MiddlewareInterface; use Psr\Http\Message\ServerRequestInterface; use Zend\Diactoros\Response\TextResponse; use Zend\Stratigility\MiddlewarePipe; $pipeline = new MiddlewarePipe(); $pipeline->pipe(new class implements MiddlewareInterface { public function process (ServerRequestInterface $request, DelegateInterface $delegate) { $response = $delegate->process($request); return $response->withHeader('X-ClacksOverhead', 'GNU Terry Pratchett'); } }); $pipeline->pipe(new class implements MiddlewareInterface { public function process(ServerRequestInterface $request, DelegateInterface $delegate) { return new TextResponse('Hello world!'); } });
虽然有更多的废话—以前我们的匿名函数本质上是什么,现在被包装在一个类中,为每个函数添加几行—结果并不十分繁琐,并为我们提供了重要的类型安全.我们的中间件运行程序不再需要假设任何通过管道传输给它的中间件都已正确定义,而是可以知道,因为它可以强制执行类型提示。
该方法对IDE也很有用,它现在可以正确地键入提示参数,并在违反约定时让我们知道。
闭包呢?
PHP中的闭包允许您关闭或绑定currentscope中的变量到匿名函数。例如,如果我想创建日志记录中间件,我可能会执行以下操作:
// Where $log is a PSR-3 logger: use Zend\Diactoros\Response\EmptyResponse; $pipeline->pipe(function ($request, $response, callable $next) use ($log) { try { $response = $next($request, $response); return $response; } catch (Throwable $e) { } $log->error(sprintf( '[%d] (%s) %s', $e->getCode(), get_class($e), $e->getMessage() ), ['exception' => $e]); return new EmptyResponse(500); });
我如何使用匿名类完成此操作?
匿名类允许您在声明期间传递参数,然后将这些参数传递给构造函数。因此,您将变量从当前作用域绑定到类中,通常作为类属性:
// Where $log is a PSR-3 logger: use Interop\Http\ServerMiddleware\DelegateInterface; use Interop\Http\ServerMiddleware\MiddlewareInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; use Zend\Diactoros\Response\EmptyResponse; $pipeline->pipe(new class($log) implements MiddlewareInterface { private $log; public function __construct(LoggerInterface $log) { $this->log = $log; } public function process(ServerRequestInterface $request, DelegateInterface $delegate) { try { $response = $delegate->process($request); return $response; } catch (Throwable $e) { } $this->log->error(sprintf( '[%d] (%s) %s', $e->getCode(), get_class($e), $e->getMessage() ), ['exception' => $e]); return new EmptyResponse(500); } });
这种方法为您提供了额外的类型安全:如果$log
是不同的类型,您将知道该中间件何时创建,因为PHP会引发致命错误。
我喜欢这种方法的另一件事是,它允许我在正式编写类之前制作类原型。我可以开始了解重用的可能性是什么、我可能需要什么参数等等。因为匿名类的语法与声明的类相同,我稍后可以通过简单地剪切定义并将其粘贴到它自己的文件中来将其提取到命名类。
所以,不要让PSR-15接口阻止您!开始为您自己的中间件原型使用匿名类!